前言:Design Patterns 在上一篇文章告一個段落了,本系列文章從今天開始會介紹五個常見的 Architectural Pattern,依序為 MVP, MVC MVVM, SOA 與 Microservices。
"MVP、MVP、MVP" 在一陣歡呼聲中出場的,是 architectural pattern 三兄弟中的二哥: MVP pattern。
為何我們會把 MVP 放在其他兩位常拿來比較的兄弟 (MVC、MVVM) 之前介紹呢? 除了他是 MVP 之外,他也能提供最清楚的介面,讓我們只需要透過少量的閱讀,就能馬上理解程式所提供的情境及功能。
先讓我們來看看以下的介面定義,是否能夠大概猜到 View 和 Presenter 之間如何串起使用情境呢:
interface ArticleContract {
interface ArticlePresenter {
fun onAddArticle()
fun onArticleSelected(article: Article)
fun onBack()
}
interface ArticleView {
fun showArticles(articles: List<Article>)
fun showArticleContent(article: Article)
fun hideArticleContent()
}
}
首先我們看到 View,由 View 本身去想像,應該就是需要在畫面上畫出什麼吧!
而 View 有著三個方法,showArticles
需要取得一串文章、showArticleContent
則是...這應該不需要過度解釋。
另一個 Presenter 介面,雖然這個字好像較為抽象,但在閱讀過其提供的三個方法後,大致能夠想像他提供了接收使用者事件的功能吧。
如果介面能夠輕易地被看懂,那代表這個 MVP 設計得算是成功。
清楚的透過介面來定義表達使用情境,並定義情境中 View 層以及 Presenter 層的交互作用,正是 MVP 最大的優點。
在省去一切實作細節的情況下,透過閱讀介面定義,便能夠讓程式在後續維護、增加或修改使用情境、甚至是抽換特定層實作...等種種情況下帶來好處。
MVP 架構著重在於程式的不同階層間透過合約以及介面認識其它階層開放讓外界知道的方法,而將實作細節封裝於元件內部,用以達到各層級可獨立於彼此實作的效果。MVP 三層各自的職責如下:
View 和 Presenter 共同為畫面呈現負責,兩者各自透過合約認識對方,在需要時直接透過介面呼叫彼此。
而 Presenter 則可以取得 model 的實體,對其進行更新並將結果轉交給 View 進行呈現。
圖上的數字 1~4 代表一個由使用者所觸發的情境的處理順序。情境由 View 接收使用者輸入開始,透過介面通知 Presenter,故 Presenter 端的介面使用情境的描述。
Presenter 在更新 Model 並取得新的資料後主動通知 View 進行呈現,故 View 層的介面通常為呈現資料或改變呈現狀態。
優勢
缺陷
這個 Github repository 中實作了 ArticleContract
的範例程式的 MVP, MVC, MVVM 版本,包含以下情境:
下面是 Presenter 的實作範例:
class MVPPresenter(private val repository: RunTimeRepository,
private val view: ArticleContract.ArticleView): ArticleContract.ArticlePresenter {
init {
// 情境1: 初始化,取得初始文章列表並呼叫 View 呈現
repository.mergeDefaultArticles()
view.showArticles(repository.getArticles())
}
override fun onAddArticle() {
// 情境2: 創造一篇隨機內容的文章並加入文章列表
val article = ArticleGenerator.randomArticle()
repository.addArticle(article)
view.showArticles(repository.getArticles())
}
override fun onArticleSelected(article: Article) {
// 情境3: 呈現特定文章的內容
view.showArticleContent(article)
}
override fun onBack() {
// 情境4: 透過隱藏文章列表以返回呈現文章畫面
view.hideArticleContent()
}
}
可以注意到 Presenter 並沒有任何依賴於任何 Android 的函式庫,因為我們將所有與 Android 平台相關的內容都封裝在 View 層中實現。
而不管是合約還是 Presenter 當中都沒有提及任何情境中提到的 "返回按鍵" 細節,所以此按鍵的實作同樣完全封裝在 View 當中。
View 的實體則是由 Android 系統提供的 Activity 進行實作,同時兼具系統起始點以及 View 的責任。
class MVPArticleViewActivity : AppCompatActivity(), ArticleContract.ArticleView, ArticleAdapter.ItemClickListener {
// ... 省略部分實作細節
private fun subscribeUseCases(presenter: MVPPresenter2) {
// 情境2
RxView.clicks(add_article_btn)
.subscribe {
presenter.onAddArticle()
}.addTo(disposableBag)
// 情境4
RxView.clicks(back_btn)
.subscribe {
presenter.onBack()
}.addTo(disposableBag)
}
override fun onItemClick(view: View, article: Article) {
// 情境3
presenter.onArticleSelected(article)
}
// 情境1
override fun showArticles(articles: List<Article>) {
adapter.setData(articles)
adapter.notifyDataSetChanged()
}
override fun showArticleContent(article: Article) {
// 返回按鍵的實作
back_btn.visibility = View.VISIBLE
articleView.showArticle(article)
}
override fun hideArticleContent() {
back_btn.visibility = View.INVISIBLE
articleView.showArticle(null)
}
}
這裏提供幾個 MVP Pattern 在 Android 實作時容易遇到的問題,這些狀況在理想上都可以有好的解法,但在一線戰鬥現場卻難以達成,大家在應用 MVP 時不仿可以好好思考一下:
Presenter 該不該接觸 lifecycle:
當我們覺得 Presenter 應該扮演所有處理 Model 相關事情時,可能會讓 Presenter 一併處理 save/restore 或是 onPause 諸如此類的系統生命週期。如此一來 Presenter 容易越加混亂,也需要考量更多與使用者無關的情境。
有一種做法是將 Activity 同時作為系統起始點看待,讓 Activity 在啟動時得到 Repository 的實體並處理好一切的系統生命週期。但需要注意 Presenter 內部最好不要有散落的 View State,需要將這些 State 一併包在 Model 中處理、或是在宣告 Presenter 時一併作為參數帶入。
讓 Presenter 接觸 context:
許多時候我們會想在 Presenter 層利用 context 來拿取各種資源,但這樣做會使得 Presenter 無法從 Android 依賴中抽離,建議將 context 封裝在物件中並提供介面,再由 Activity 注入。
複雜使用情境下的 Presenter(s):
複雜情境下容易遇到 "是否維持單一 Presenter、或是將 Presenter 拆解成多個" 的兩難問題。
維持單一 Presenter 則容易遇到 God Class、分解則會在多個 Presenters 共用同一個 Model 時無法知道何時更新的問題。
在此情況下,建議換用其它 pattern,或是破壞掉 Presenter-Model 間的關係,讓 Presenter 可以直接觀察 Model 以觸發更新。
作者:Yolung